MVC、SPA 与 SSR

这篇的灵感来自於 Front-End Developers Taiwan 裡面的一串讨论,有人 po 了一个影片是来讨论「MVC vs SPA」,这个标题一出来大家都惊呆了,想说怎麼会有这样的比较,於是下面掀起一波激烈的讨论,最后发现原 po 误用了专有名词,才导致这样的结果。

虽然说很多技术名词本来就是一个名词各自表述,但基本概念通常都不会相差太远,只有在细节上会有些许的争议以及讨论而已。

身為致力於要让新手更容易搞懂技术名词的人,不如就让我来尝试看看讲解这几个东西吧!

这篇文章的目标是:「只要你有网页前端的基础,就能够搞懂我在说什麼」,如果你搞不懂的话,别担心,不是你的错,是我没写好。麻烦在下面留言一下让我知道哪裡可以再改进。

接下来我会以主角小明為中心点出发,试著从一段虚拟的故事不断带出:「為什麼 XXX 会出现」、「為什麼我们需要 XXX」这些问题。如果你只对真实歷史的名词演进有兴趣,那你可能要去维基百科才能找到比较正确的资料。本故事纯属虚构,如有雷同…应该不太可能会有雷同啦,就让我们开始吧!

(先打个预防针,故事有点长,如果这些概念你都理解了应该会觉得这篇文章超级废又超级长)

第一幕:在很久很久以前…

小明是一个初学程式的新手,在这之前有用 Dreamweaver 写过一些简单的静态网页,对 HTML、CSS 以及 JavaScript 都有一些基础,而朋友们都推荐他去学 PHP 来补足后端的部分。

经过了一个月的苦练之后,小明终於完成了他的第一个后端程式,是一个非常简单的留言板系统(怕大家伤眼睛,这边只截给大家看其中一部分)

别笑,这就是你年轻时会写出来的东西

PHP 程式码、商业逻辑、HTML,全部东西都混在一起做撒尿牛丸,写了这样的程式码之后,小明每次考试都考一百分呢!

小明一开始觉得很兴奋,自己终於能够通晓前后端,成為全端工程师,便兴冲冲的持续精进自己的后端技术,每天都加一个新的 feature 进去。过了两个礼拜之后,小明整理了一下资料夹,发现总共有 100 个 PHP 档案,每个档案有超过 300 行 code,而且全部都是 PHP 跟 HTML 混在一起写。

他随便点开其中一个档案,看了 10 秒之后大喊:

我到底写了三小

意识到自己写的 code 很烂,是迈向一个好的工程师的第一步。

第二幕:痛改前非

发现自己写的程式码连自己都看不懂的时候,小明觉得这样不行,阿岳也觉得不行,我也觉得不行。

因此呢,小明跑去十分瀑布下面打坐了三天三夜,不断想著该怎麼样让他的程式码变得更好,能够更好维护、更好读懂。他不求一步登天,只希望三个月之后当他回顾自己写的 code 时,不要骂脏话就好。

就在第三天的晚上,他突然有了灵感,大喊了一声:

Eureka!

就赶紧跑回家去重构自己的 code 了,而下面是他重构的结果:

纯属范例,绝对没办法跑

这个范例跟之前差在哪边呢?

首先,他把任何跟资料有关的操作都放到一个叫做 Model 的地方去,所以你要改任何跟资料有关的东西,都到那边就对了。

再来,他也把所有跟显示画面有关的东西都放到其他地方去,我们就叫做 View 吧,View 裡面用一个 template 来塞入资料,不做任何跟资料有关的处理。

如此一来,他就把资料跟画面显示这两者切开了,并且让开头那段 PHP code 把这两者连接起来,先去 Model 拿资料,再把资料塞入 View 裡面输出。那这个「连接两者」的角色,就叫做 Controller 吧!

於是,MVC 就这样出现了。為的就是要把原来乱七八糟的程式码理出一个头绪来,是你的就是你的,不是你的就给我滚远点。你要存取资料库就是去 Model 裡面,你要写 HTML 就去 View,绝对不会出现在 View 裡面下 SQL Query 这种事情。

所以 MVC 是什麼?就是一种设计模式,你只要把你的 code 像这样子切开,都可以叫做 MVC,所以你不可能只看一个画面就跟你朋友说:「欸欸,这个网站是 MVC,因為他有好多个页面」,除非你可以通灵看到背后程式码的架构长什麼样子。

话说后来又陆陆续续出现很多种模式,而且 MVC 其实也没有想像中的职责这麼分明,在这边我就不细讲了,我自己对那整段歷史也没有很熟,有兴趣的可以参考:

MVC是一个巨大误会
_我是web工程师,从刚开始学MVC就深感困惑: 怎麼每个地方说的MVC都不太一样? 有些文章讲的MVC,跟我正在用的MVC,怎麼像完全不同的东西?…_blog.turn.tw

然后我上面那段 code 是乱写的,如果你对真实世界的 MVC 框架写出来的 code 有兴趣的话,会长成这样:

PHP 的框架 CodeIgniter 写出来的

讲到这裡我们做一个小总结,问自己三个问题:

  1. 為什麼要有 MVC?
  2. 有 MVC 跟没有 MVC 的差别在哪?
  3. 所以 MVC 是什麼?

三个问题可以一起回答:

因為小明写的 code 太脏了太难维护,所以需要重构。而后来他发现用 Model、View、Controller 这三个概念来切的话可以把 code 写得漂亮很多又好维护,就这样做了。差别在於原本的 code 混在一起,遵守 MVC 的规范之后职责变得清楚很多。所以呢,MVC 就是一种架构,后端可以遵守 MVC 的架构去开发,前端也可以,就算不是 Web 也可以用 MVC。

第三幕:毛很多的使用者

小明把自己的烂 code 利用 MVC 模式重构之后,看起来还挺不赖的,至少比以前好很多,三个月过去了,也能看得懂自己以前写的 code。把留言板的程式码重构得差不多之后,小明决定把这个专案公开,开放给大家註册使用,让每个人都可以有自己的留言板。

一开始状况都还行,大家纷纷感激小明的无私奉献,「祝楼主一生平安」、「感谢大大无私的分享」,可是好景不常,有天小明收到了一个回馈:

我每次留言之后页面都会刷新,我家网速又慢,每次都要等个十几秒,有没有可能不要重新整理页面?你看人家 Gmail,我寄完信它也没有重新整理啊!人家做得到你应该也做得到吧

身為一个滥好人,小明乖乖的去研究 Gmail 到底是如何做到的,发现秘诀就在於一个神奇的东西:Ajax,全名 Asynchronous JavaScript and XML。

全名听起来很吓人,但说穿了其实就是你在 JavaScript 裡面可以非同步的去呼叫 Server 的 API 并且拿资料回来,在 Ajax 出现之前,你要把资料带过去都必须透过 Form 的方式,一定要换页。可是有了 Ajax 以后,不换页也能跟 Server 沟通。

Gmail 就是利用这样的原理,才能达成寄信不换页。

小明研究了一个假日之后便著手改造自己的留言板,把原本利用 Form 发送留言的地方变成 Ajax,可是他碰到了一个问题:

原本我新增留言之后重新整理页面就可以看到新的留言了,因為 Server 会把最新的结果传回去;可是我现在用 Ajax,我要怎麼在不刷新页面的前提下在画面上新增留言?

总而言之呢,利用 Ajax 之后的确是发了一个 Request 跟 Server 说你要新增留言,也成功了,可是画面上不会平白无故就跑出一个新的留言。在经过短暂的思考后,小明得到一个很直觉的解法:「阿我就用 JavaScript 来新增就好啦!」

经过一番修改之后,新增留言的程式码从原本很简单的一个 Form 表单,变成下面这个样子:

Ajax 送出资料之后利用 jQuery prepend 上去

使用者的需求被解决了,小明也有了技术上的成长,可谓是一石二鸟、一举两得,但小明天真的地方就在於他把使用者想得太简单了。

过了一个礼拜之后,同一个使用者又写信给小明:

很感谢你上次新增的功能,可是我有个疑问。我看 Gmail 无论做什麼操作都不会换页,你的留言板也可以改成这样吗?这样比较方便,谢谢。

滥好人小明没有仔细深究「比较方便」到底是怎样的方便,纯粹站在一个希望满足使用者所有需求的角度跳下去研究 Gmail 到底还能够做到什麼。

他发现了 Gmail 跟其他网站不同的地方就是:「无论做什麼操作都不会换页」,换页指的是「你会有一段时间看到整个画面全白,因為瀏览器正在等待 Server 的 Response 才能载入 HTML」。

你在用 Gmail 的时候,无论你是写信、读信、整理信件或是切换到设定页,儘管你的网路跟乌龟一样,你还是看不到任何全白画面。

為什麼?因為 Gmail 所有跟 Server 沟通的地方都是用 Ajax。

这是改造前的范例,我们利用表单 POST 来新增一笔留言,所以你会看到一小段的白画面:

此范例改自我学生 Kris 的作业,http://thinkr.tw/

这是改造后的范例,因為我们用 Ajax 来新增留言的关係,所以你不会看到任何白画面的出现,使用者体验好很多:

利用 Ajax 跟 JavaScript 在前端新增留言

我们再举一个简单的小例子,假设小明今天写了一个没有用 Ajax 的 Minmail,他删除一封信的流程是这样的:

  1. 点击删除之后,利用 Form 表单 POST 资料去 /server/delete_email
  2. /server/delete_email 处理完之后 redirect 回去信件列表
  3. 瀏览器重新载入信件列表(在载入之前你都会看到全白画面)

可是如果是像 Gmail 那样子全部改成 Ajax 的话,就会变成:

  1. 点击删除之后,利用 Ajax POST 资料去 /server/api/delete_email
  2. /server/api/delete_email 处理完之后回传 Response
  3. 利用 JavaScript 在前端把那封信的从画面上移除

后者利用 Ajax 跟后端同步资料,并且在前端用 JavaScript 更改画面,所以你无论做什麼操作都不会换页,也可以保证前后端的资料是同步的。

知道区别以及原理之后,小明把整个网站都改造成这种形式,只要是任何原本用到 Form 的地方,现在全部都用 Ajax 拿资料并搭配 JavaScript 来做画面上的处理。

因此,留言列表现在变成 Ajax 拿资料回来之后由 JavaScript 把留言 append 到画面上,就像我们刚刚示范的新增留言那样。

此时,小明突然有个非常惊人的发现:

咦,如果我全部画面都是由前端利用 JavaScript 动态產生的话,那我原本后端的 View 要干嘛?

咦,对啊,既然现在所有画面都是在前端由 JavaScript 动态產生,那我后端不就永远都输出同一个档案就好?如此一来,使用者看到的其实都是同一个页面,而我们利用 JavaScript 在这个页面上做变化。

这个概念就叫做 SPA,全名是 Single Page Application,单页式应用。与之对应的概念是 MPA,Multiple Page Application。

SPA 与 MPA 的对照

就如同小明领悟的一样,前端如果利用 SPA 来实作的话,会把原本应该是后端处理的一部份职责给搬到前端去,例如说状态的管理跟路由。

举例来说,在以往 Server 根据不同的路径对应到不同的 Controller,进而渲染出不同的 View。可是现在 Server 无论什麼路径都会输出同一个档案,所以你在前端也要判断现在的网址是哪个,才能决定在前端应该渲染出哪个画面。

再举一个例子,假设我现在写了一个电影列表的网站,首页列出许多热门电影,点进去可以看到个别电影的详细资料。而我们做了以下动作:

  1. 点进电影 A
  2. 快速按上一页
  3. 快速点进电影 B

如果是 SPA 的话,实作的逻辑应该会是:「点进单独电影时发送一个 Request,等 Response 回来之后把资料显示在画面上」,乍听之下没什麼问题,但若是你在第三步的时候,第一步所发出去的电影 A 的 Response 才传回来,你的画面就会显示出电影 A 的资讯,可是使用者点的明明就是电影 B。

这就是我所说的状态管理变复杂了,有些地方需要花点心思做处理。在以往 MPA 的时候完全不会发生这种事,你可以保证 Server 会回传正确的结果,因為画面是在后端 render 再回传回来的,而且每一个页面之间的状态不会互相干扰。

如果写得好,我相信 SPA 的使用者体验一定很不错,因為用起来就跟你在用 Native App 差不多嘛,但你必须付出的代价是前端变得超级复杂,有一堆非同步的问题要考虑还有一大堆事情要做。此时的前端复杂度已经跟我们最开头示范的那种简单留言板相差许多了。

在这种时候,前端也可以参考我们前面所说的 MVC 架构或是其他相关架构来让程式码的职责变得更分明,让整个专案更好维护。所以你可以又有 MVC 又有 SPA,或是没有 MVC 但有 SPA,这两者是完全不同的概念。

我之前写过另外一篇文章,有兴趣的话可以参考看看:

前后端分离与 SPA
_TechBridge Weekly 技术週刊团队是一群对用技术改变世界怀抱热情的团队。本技术共笔部落格初期专注於Web前后端、行动网路、机器人/物联网、数据分析与產品设计等技术分享。_blog.techbridge.cc

最后我举一个一定要用 SPA 的例子:音乐播放网站。

如果音乐播放网站是用 MPA 的话,每去一个新的网址就会把整个页面换掉,那你的网页播放器就会中断了,这是完全没办法接受的事。所以唯一的解法就是:播放器永远都在页面上,只有其他部分的内容换掉。而这一切都是在前端用 JavaScript 来处理的。

第四幕:行销团队的暴怒

小明花了整整一个月的时间不眠不休不吃不喝(夸饰法,开玩笑的),终於把整个网站改造成 SPA,而且还优化了不少地方,让整个使用者体验变得非常非常好。

不久过后,这个留言板系统因為体验实在是太好了,有越来越多人使用,短短一个月内就有了一百万个来自世界各地的使用者註册。还有来自国外的使用者甚至写信给小明希望能够付钱来拥有更多功能:

Hey, thanks for building such a cool website, I really like it. Is there any premium plan? I am glad to pay for the additional features like custom domain or custom template.

听过一大堆创业讲座的小明知道时候到了,可以把这个 side project 当作创业项目了!

凭著现有的成绩,小明很快地就募到了天使轮,找了几个伙伴成立了一间公司,想要把这个留言板系统做成全世界第一的留言板,期许自己能成為留言板界的 WordPress。

可是好景不常,过了一两个月之后不知道為什麼,新的会员越来越少,砸下大笔的广告费也只带来短暂的成效而已,一旦广告停了就又恢復以往冷清的样子。

奇怪,就算是热潮退烧也没退烧得这麼快才对,到底是发生什麼事呢?

一个礼拜过后,专长是数位行销的合伙人气噗噗的跑到小明的位子前,口气很差地质问他:

你做了什麼?為什麼在搜寻引擎上面搜寻我们的网站,结果只会出现一大堆看不懂的程式码?我们的网站 SEO 做的奇差无比你知道吗?

小明一开始觉得很委屈,他什麼都没做,怎麼会落得如此下场。但经过左思右想之后,终於发现了癥结点:SPA。

由於 SPA 是由前端的 JavaScript 动态產生内容,因此如果你对 SPA 的网站按下右键 -> 检视原始码,只会看到空荡荡的一片,只看得到一个 JavaScript 档案跟一些最基本的 tag。

内容在哪裡?不在这裡,因為那是由 JavaScript 动态產生的。只有你的网站经由瀏览器载入并且执行 JavaScript,等 Response 回来之后才会动态產生出内容。因此无论是哪个页面,你检视原始码都看不到动态新增后的内容。

惨了,这可是天大的坏消息。

但其实也没有那麼坏,因為强大的 Google 的爬虫其实支援执行 JavaScript,所以他依然会 index 你在前端渲染之后的页面。

不过还是有两个问题,第一个是我们不知道 Google 如何执行,会不会前端还没完全渲染完就已经爬完了?第二个是除了 Google,还有其他很多搜寻引擎,有些可能没有像 Google 这麼强大,碰到 SPA 就只能索引空荡荡的 HTML,内容几乎空白。

该怎麼办呢?

苦恼的小明跟公司请了长假,再度跑到十分瀑布下面修行,希望能够重演当年想出 MVC 架构的剧本。很幸运地,过了三天之后,小明终於想到解法了,大喊了一声:

干我知道了!

小明的想法是这样的,既然问题出在「第一次渲染」,那我们只要在第一次渲染的时候把该输出的资料都输出就好啦,对使用者来说还是一个 SPA,差别在於使用者接收到 HTML 的时候,就已经有完整的资料了。

举例来说,假设使用者拜访显示所有留言的页面,我在 Server Side 先把所有留言都準备好然后 render 出来,这样使用者一收到 Response 的时候就能够看到所有留言,搜寻引擎也能顺利地爬到。

而后续的操作还是由 JavaScript 来处理,依旧能保持 SPA 的优点。或者我们能用一句话来总结:

第一个页面由 Server side render,之后的操作还是由 Client side render

没错,这个概念就叫做 SSR,Server Side Rendering。

CSR vs SSR

有了 SSR 以后,就解决了 SEO 的问题,对网路爬虫来说你有没有用 SPA 都无所谓,他所抓到的内容都是一样的。可是对使用者来说,一样能享受到 SPA 所带来的好处(不用换页)。

虽然我在这边只用几句话带过去,看起来轻鬆写意,但真的实作过的话你就会发现这不是一件容易的事,有很多细节要去考虑。总之呢,小明花了整整两个月的时间才把整个网站都改成 SSR。

不久后,在每个月的员工大会(新创嘛,一个月一次很合理)中 CMO 很开心地跟大家宣布產品在搜寻引擎上面的排名越来越高,自然流量也越来越多,註册会员比起上个月增长了 200%。

CMO 很开心,搜寻引擎很开心,员工很开心,小明当然也很开心。

一天又平安的过去了,感谢飞天小明警的努力。

最终幕:前端的未来

因為科技进步快速加上网路的普及,世界变动的比以前快很多。

十年前手机还只是让你打电话以及斤斤计较简讯字数的工具,十年后就变成人手一台,不可或缺的小型电脑。

身為经歷过这一切的人,小明在深夜裡边刷著 leetcode 边回忆起前端的发展,遥想十年前他以為 HTML 跟 CSS 才是主角,JavaScript 只是阻止使用者点右键或是做出会跟著鼠标移动的酷炫跑马灯的小玩具。

可是一切发展的越来越快,jQuery 的出现一统江湖,解决了恼人的跨瀏览器问题,CSS 也因為预处理器的出现而变得更好维护,可以用更程式化的角度来撰写。再过个两三年,大家都不谈 jQuery 了,而是谈 Angular。可是又过了几年,最潮的名词变成 React,到现在 React 也没那麼潮了,要潮的话请去写 Vue。

更别提 SPA 的遍地开花以及 SSR 的出现,更是将前端的复杂度提升了不只一个档次。有了 SSR 你就不再只是前端了,毕竟 SSR 的 S 可是 Server 的意思,还必须要会一点 Server side 的技术才行。

在 Mobile 的流量渐渐超越 Desktop 之后,前端的目标就迈向「可以逼近 Native App」的体验。又像是个 App 可是又不用安装,那该有多好,省了安装这个步骤转换率大幅提升,使用者开心公司也开心。

於是大家开始提倡 PWA,Progressive Web App。Web 不再单纯只是 Web,而是要用起来像个 App,看起来也像个 App。甚至利用 Service Worker 搭配快取,在没有网路时也能够使用部分功能,也可以用 Skeleton 先把画面的骨架显示出来。

这一切的一切都為了一个目的:增进使用者体验。

前端复杂归复杂,但身為真心喜爱前端的人,小明可是对未来充满了希望。一想到能够接触更多新的技术,更多新的解法,可以打造出更好的產品,小明内心涌起的情绪不是挫折而是兴奋,无比的兴奋。

东方的太阳缓缓升起,散射出的光芒洒在小明的房裡,提醒著他新的一天即将开始。

结语

对我来说,一个技术的出现绝对是有其理由的。而不是简单一句:「前端现在就是这麼复杂」,我认為只要能理解他出现的脉络,就能更轻易的从宏观的角度去理解这项技术。

我们可以用三个问题来帮助自己理解一项事物:

  1. 為什麼要有 XXX?
  2. 没有 XXX 跟有 XXX 的区别是什麼?
  3. 所以 XXX 是什麼?

MVC 就是因為 code 变得越来越乱,所以将职责区分清楚的一种设计模式。SPA 就是因為想增进使用者体验,而出现的一种在前端利用 Ajax 达成不换页的方法。SSR 就是因為要解决 SPA 的 SEO 问题而出现的解法。

一切都是有理由的,一切都是有原因的。你可以不懂它怎麼实作,但你一定要懂它是為了什麼而生。程式是工具,工具的目的是解决问题,重要的不是工具本身,而是背后要解决的那个问题。

感谢大家的阅读,如果有任何错误麻烦不吝指出。另外,此篇文章只希望能给出一个大方向,对於细节如果要讨论的话其实每个细节都可以再写一篇专文,例如说 MVC 到底是在讲哪个 MVC?SPA 在 Google 上的 SEO 真的比较差吗?SSR 在首次加载页面上牺牲的时间(因為要等 API 的资料回来才能 render)与增进 SEO 之间的取捨等等。

喜欢的话可以拍个手,你知道 Medium 最多可以拍 50 下吗?可以根据你的喜好程度拍不同次数的手,想要拍好拍满的话我也是乐见其成。

最后,再次感谢你的阅读。

转载说明

此文章是在一技术讨论群里看到的,看后觉得很有感悟,便转载了过来,但当时只是把内容复制了过来,原文的链接忘了,后来找了群里面的聊天记录和浏览器的访问历史,都没有找到,在此就不注明原文章的链接了。